[Swift] BrightFuturesで非同期処理をflatMapでつなげる方法
はじめに
こんにちは!
モバイルアプリサービス部の田中孝明です。
こちらのブログの続きのエントリーになります。
Swift 3.0に移行する上で、従来の機能をおさらいしておこうという内容のブログになります。
Future と Promise
Java、Scala、JavaScriptを実装されて事ある方には馴染み深い非同期処理の機構です。
Future
は未来の結果を表し、Promise
は成功と失敗を表す処理や値をFuture
に変換する事ができます。
詳しくはFuture と Promiseを参照していただければ幸いです。
SwiftとFuture/Promise
Swiftには残念ながらFuture/Promiseの機構は備わっていません。
しかし、BrightFuturesというライブラリを組み込むことで、Future/Promiseを利用する事ができます。
導入方法に関しては弊社のブログ、[Swift] 非同期処理フレームワークBrightFutures ~導入編~が参考になると思います。
内容がswift3.0以前のものなので、復習も兼ねて書き記していきます。
導入
API通信をFuture
で非同期処理するサンプルを作りたいと思います。
今回もCocoaPods
で導入します。
pod 'Alamofire', '~> 4.0.1' pod 'SwiftyJSON' pod 'BrightFutures' post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '3.0' end end end
Alamofire
とSwiftyJSON
に関してはAPI通信の際に利用するために導入しています。
Future
とPromise
を利用する場合はBrightFutures
のみで十分です。
モックの作成
ローカルの環境で動作テストするだけですのでjson-serverを利用してJSONを返すだけのモックを用意します。
モック用のjsonファイルは以下のようなものを用意しました。
{ "users": { "id": 1, "name": "test-1", "token": "XXXXXXXXXXXXX" }, "profile": { "firstname": "TAKAAKI", "lastname": "TANAKA" }, "messages": [ { "id": 1, "text": "hello" }, { "id": 2, "text": "nice" } ] }
それぞれusers
、profile
、messages
でアクセスした際に定義された以下のJSONを返却する動作になります。
json-serverはPOSTも対応していますが、今回はGETのみで利用します。
iOSアプリ側でJSONの結果を受け取ってパースした結果を保持するstructを定義しておきます。
struct User { let id: Int? let name: String? let token: String? init(json: JSON) { id = json["id"].int name = json["name"].string token = json["token"].string } } struct Profile { let firstname: String? let lastname: String? init(json: JSON) { firstname = json["firstname"].string lastname = json["lastname"].string } } struct Message { let id: Int? let text: String? init(json: JSON) { id = json["id"].int text = json["text"].string } }
Future/Promiseを適用しない場合
クロージャを利用して結果を受け取るようにすることが多いのでは無いでしょうか。
var usersComplitionHandler: ((User) -> Void)? var profileComplitionHandler: ((Profile) -> Void)? var messagesComplitionHandler: (([Message]) -> Void)?
// すごく雑な作り方 usersComplitionHandler = { user in print(user) } Alamofire.request("http://localhost:3000/users").responseJSON { [weak self] response in switch response.result { case .success(let value): let json = JSON(value) let user = User(json: json) self?.usersComplitionHandler?(user) case .failure(let error): print(error) } }
Future/Promiseを適用した場合
users
の処理を置き換えてみます。
リクエストの成功をpromise.success
に、失敗をpromise.failure
に定義します。
promise.futureでFutuer
に変換したものを返却します。
private func getUser() -> Future<User, NSError> { let promise = Promise<User, NSError>() let queue = DispatchQueue(label: "getUser", attributes: .concurrent) Alamofire.request("http://localhost:3000/users").responseJSON(queue: queue, completionHandler: { response in switch response.result { case .success(let value): let json = JSON(value) let user = User(json: json) promise.success(user) case .failure(let error): print(error) promise.failure(error as NSError) } }) return promise.future }
あとは呼び出し元で成功時の処理をonSuccess
と失敗時の処理をonFailure
に定義します。
Future
の名前が示す通り、受け取り側は「いずれ結果が返ってくるもの」に対して成功と失敗の処理を書きます。
let userFuture = getUser() userFuture.onSuccess { user in print(user) }.onFailure { error in print(error) }
どちらの処理が書きやすいかは主観の域を出ないので、ここでは言及しません。
複数の非同期処理を直列に行う
Future/Promiseを適用しない
Future/Promiseを適用しないで、2つ以上の非同期処理を直列で呼びたい時は、クロージャ内でクロージャを呼ぶなど、多少煩雑になったりします。
試しにusers
→profile
→messages
を順番に呼び出す処理を記します。
// 絶対真似してはダメ usersComplitionHandler = { [weak self] user in Alamofire.request("http://localhost:3000/profile").responseJSON { [weak self] response in switch response.result { case .success(let value): let json = JSON(value) let profile = Profile(json: json) self?.profileComplitionHandler?(profile) case .failure(let error): print(error) } } } profileComplitionHandler = { [weak self] profile in Alamofire.request("http://localhost:3000/messages").responseJSON { [weak self] response in switch response.result { case .success(let value): let json = JSON(value) let messages = json.arrayValue.map { Message(json: $0) } self?.messagesComplitionHandler?(messages) case .failure(let error): print(error) } } } messagesComplitionHandler = { [weak self] messages in print(messages) } Alamofire.request("http://localhost:3000/users").responseJSON { [weak self] response in switch response.result { case .success(let value): let json = JSON(value) let user = User(json: json) self?.usersComplitionHandler?(user) case .failure(let error): print(error) } }
クロージャの処理が絡むため、処理の順番が追いにくくなってしまいます。
Future/Promiseを適用
ではFuture/Promiseを適用した場合はどうでしょうか。
まずはusers
と同様にprofile
とmessages
もFuture
で返すように定義します。
private func getProfile(id: Int?) -> Future<Profile, NSError> { let promise = Promise<Profile, NSError>() let queue = DispatchQueue(label: "getProfile", attributes: .concurrent) Alamofire.request("http://localhost:3000/profile").responseJSON(queue: queue, completionHandler: { response in switch response.result { case .success(let value): let json = JSON(value) let profile = Profile(json: json) promise.success(profile) case .failure(let error): print(error) promise.failure(error as NSError) } }) return promise.future } private func getMessages(name: String?) -> Future<[Message], NSError> { let promise = Promise<[Message], NSError>() let queue = DispatchQueue(label: "getProfile", attributes: .concurrent) Alamofire.request("http://localhost:3000/messages").responseJSON(queue: queue, completionHandler: { response in switch response.result { case .success(let value): let json = JSON(value) let messages = json.arrayValue.map { Message(json: $0) } promise.success(messages) case .failure(let error): print(error) promise.failure(error as NSError) } }) return promise.future }
Future
にはflatMap
が定義されているため、結果をflatMap
で繋げて直列に処理する事ができます。
getUser().flatMap { user -> Future<Profile, NSError> in self.getProfile(id: user.id) }.flatMap { profile -> Future<[Message], NSError> in self.getMessages(name: profile.name) }.onSuccess { messages in print(messages) }.onFailure { error in // 全てのAPIの失敗はこちらに集約される print(error) }
それぞれのFuture
に対してflatMap
で繋げることで、非同期処理を順番に行なう事ができます。
onSuccess
には最終的な非同期処理の成功の結果を、onFailure
では全ての非同期処理の失敗が処理されるようになります。
Future/Promiseを利用した事ある方には馴染みのある記法になったのではないでしょうか。
ただし、こちらのブログでも記載した通り、Swiftにはfor(yield)
のようなシンタックスシュガーが提供されていないので、多少煩雑に感じるでしょう。
まとめ
Future/Promiseパターンを導入したからといって劇的に非同期処理が楽に書けるようになるとは限らないですが、Java、Scala、JavaScriptで実装した事のある人に馴染みのある記法を使う事で可読性を上げる事ができるのではないでしょうか。
ぜひ標準でサポートしていただきたい機構ではあります。
参考文献
Future と Promise
[Swift] 非同期処理フレームワークBrightFutures ~導入編~
【Scala】flatMap は怖くない!
【Scala】Future と未来のセカイ